# -*- coding: utf-8 -*-

"""
类定义
"""


"""

版权所有 © 2025 金羿ELS
Copyright (c) 2025 Eilles(EillesWan@outlook.com)

MineMusicVisualizer is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
         http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.

"""
from math import sqrt
from enum import Enum
from typing import Dict, Any, Literal, Union, Sequence, List, Optional
from dataclasses import dataclass

from Musicreater.subclass import MineNote

from .exceptions import AxisParallelError


@dataclass
class FloatingNote:
    """
    音符动画数据存储类
    """

    colour: str
    """
    我的世界格式化代码颜色
    """

    sound_id: str
    """
    我的世界音效ID
    """

    pitch: int
    """
    Midi 音调
    """

    remain: int
    """
    还差多少个循环消失
    """

    initial: bool
    """
    该音符是不是刚出场
    """

    parallel_notes: List["FloatingNote"]

    extra_info: Dict[str, Any]
    """你觉得放什么好？"""

    def __init__(
        self,
        mc_colour: str,
        mc_sound_id: str,
        midi_pitch: int,
        remain_tick: int,
        extra_information: Dict[str, Any] = {},
    ):
        """
        音符动画数据存储类

        Parameters
        ==========
        colour: str
            我的世界格式化代码颜色
        sound_id: str
            我的世界音效ID
        pitch: int
            Midi 音调
        remain: int
            还差多少个循环消失
        extra_information: Dict[str, Any]
            附加信息，尽量存储为字典

        Returns
        =======
        FloatingNote 类

        """
        self.colour = mc_colour
        """
        我的世界格式化代码颜色
        """
        self.sound_id = mc_sound_id
        """
        我的世界音效ID
        """
        self.pitch = midi_pitch
        """
        Midi 音调
        """
        self.remain = remain_tick
        """
        还差多少个循环消失
        """
        self.initial = True
        """
        该音符是不是刚出场
        """

        self.parallel_notes = []
        self.extra_info = extra_information if extra_information else {}

    @classmethod
    def from_mine_note(cls, mine_note: MineNote, mc_colour: str) -> "FloatingNote":
        """
        从我的世界音符类转换

        Parameters
        ==========
        mine_note: MineNote
            我的世界音符类

        Returns
        =======
        FloatingNote 类

        """
        return cls(
            mc_colour=mc_colour,
            midi_pitch=mine_note.note_pitch,
            mc_sound_id=mine_note.sound_name,
            remain_tick=mine_note.duration,
        )

    @classmethod
    def empty(cls) -> "FloatingNote":
        """
        创建一个空的音符

        Parameters
        ==========
        无

        Returns
        =======
        FloatingNote 类

        """
        result = cls("", "", 0, -1)
        result.initial = False
        return result

    def colourify(self, sth: str) -> str:
        if self.colour:
            return self.colour + sth + "§r"
        else:
            return sth

    def iterate(self) -> "FloatingNote":
        """
        迭代音符

        Parameters
        ==========
        无

        Returns
        =======
        FloatingNote 类

        """

        for note in self.parallel_notes:
            note.iterate()

        if self.remain < 0:
            # 小于 0 时，完全不动作
            return self
        elif self.remain > 0:
            # 正常迭代
            self.remain -= 1
        else:
            # 准备消失
            if self.parallel_notes:
                self.paste(self.parallel_notes.pop(0))
            else:
                self.colour = ""
                # self.pitch = 0 # 没用
                self.remain = -1

        if self.initial:
            self.initial = False

        return self

    def parallel(self, other: "FloatingNote"):
        """
        添加并行音符

        Parameters
        ==========
        other: FloatingNote
            在此位置并行的音符

        Returns
        =======
        None
        """
        if self.remain >= 0:
            # 如果自身是有音符的，就把自己放入下面去，等会再浮起来
            self.parallel_notes.append(self.copy())
        else:
            # 如果自己是空的，就直接完整复制过来了，这里 extend 实际上和 = 没区别
            self.parallel_notes.extend(other.parallel_notes)
        self.paste(other)

    def set_info(self, key: Union[str, Sequence[str]], value: Any):
        """
        设置附加信息内容

        Parameters
        =========
        key: str | Sequence[str]
            附加信息键
        value: Any | Sequence[Any]
            附加信息值（键为序列时，值也须为序列，且二者长度必须一致）

        Returns
        =======
        None
        """
        if isinstance(key, str):
            self.extra_info[key] = value
        elif isinstance(key, Sequence):
            if isinstance(value, Sequence):
                if (k := len(key)) == (o := len(value)):
                    for i in range(k):
                        self.extra_info[key[i]] = value[i]
                else:
                    raise ValueError(
                        "附加信息键值队列长度不一致，键序列长度为 {}，值序列长度为 {}".format(
                            k, o
                        )
                    )
            else:
                raise TypeError(
                    "附加信息值类型错误，在键为序列类型时，值应为等长序列，而非 {} 类型".format(
                        type(value)
                    )
                )
        else:
            raise TypeError(
                "附加信息键类型错误，不得以 {} 类型作为参数".format(type(key))
            )

    def get_info(self, key: str) -> Any:
        """
        获取附加信息内容

        Parameters
        =========
        key: str
            附加信息键

        Returns
        =======
        Any
            附加信息内容，若不存在则返回 None
        """
        if key in self.extra_info:
            return self.extra_info[key]
        else:
            return None

    def __add__(self, other):
        if isinstance(other, str):
            return self.__str__() + other
        elif isinstance(other, FloatingNote):
            return self.__str__() + other.__str__()
        else:
            raise TypeError(
                "不支持使用 {} 作为 FloatingNote 类的加法运算对象，这是在干嘛？".format(
                    type(other)
                )
            )

    def __radd__(self, other):
        if isinstance(other, str):
            return other + self.__str__()
        elif isinstance(other, FloatingNote):
            return other.__str__() + self.__str__()
        else:
            raise TypeError(
                "不支持使用 FloatingNote 作为 {} 类的加法运算对象，这是在干嘛？".format(
                    type(other)
                )
            )

    def __str__(self) -> str:
        return self.colourify(
            ("@" if self.initial else (" " if self.remain < 0 else "|"))
        )

    def __bool__(self) -> bool:
        return self.remain >= 0

    def copy(self) -> "FloatingNote":
        """
        复制当前对象

        Parameters
        ==========
        无

        Returns
        =======
        FloatingNote 类

        """
        c_result = FloatingNote(
            mc_colour=self.colour,
            mc_sound_id=self.sound_id,
            midi_pitch=self.pitch,
            remain_tick=self.remain,
            extra_information=self.extra_info.copy(),
        )

        c_result.initial = self.initial

        return c_result

    def paste(self, other: "FloatingNote"):
        """
        将其他音符的内容复制给当前对象

        Parameters
        ==========
        other: FloatingNote
            要复制的音符

        Returns
        =======
        None
        """
        self.colour = other.colour
        self.sound_id = other.sound_id
        self.pitch = other.pitch
        self.remain = other.remain
        self.extra_info = other.extra_info.copy()
        self.initial = other.initial


class CoordinateAxis(Enum):
    """
    坐标轴枚举类
    """

    X = "X"
    Y = "Y"
    Z = "Z"

    def __str__(self) -> str:
        return self.name

    def switch(self, normal_axis: Optional["CoordinateAxis"]):
        """
        将当前轴切换为法线垂直平面上的另一个轴
        该函数的实际算法与下面的 perpendicular 一致，只是仅仅改变自身，而不返回新轴。

        Parameters
        ==========
        normal_axis: CoordinateAxis
            法线轴，默认为 Y 轴

        Returns
        =======
        None

        """
        if normal_axis is None:
            normal_axis = CoordinateAxis.Y

        if self == normal_axis:  # 轴相同，无需切换
            raise AxisParallelError(
                "无法在法线 {} 轴垂直的平面上，找到与 {} 轴垂直的轴：这是同一个轴啊？".format(
                    normal_axis, self
                )
            )

        if normal_axis == CoordinateAxis.Y:
            if self == CoordinateAxis.X:
                self = CoordinateAxis.Z
            else:  # self == CoordinateAxis.Z
                self = CoordinateAxis.X
        elif normal_axis == CoordinateAxis.X:
            if self == CoordinateAxis.Y:
                self = CoordinateAxis.Z
            else:  # self == CoordinateAxis.Z
                self = CoordinateAxis.Y
        else:  # normal_axis == CoordinateAxis.Z
            if self == CoordinateAxis.X:
                self = CoordinateAxis.Y
            else:  # self == CoordinateAxis.Y
                self = CoordinateAxis.X

    def perpendicular(
        self, coplanar_axis: Optional["CoordinateAxis"]
    ) -> "CoordinateAxis":
        """
        获取 当前轴与另一只轴 所成平面的 法线轴
        该函数的实际算法与上面的 switch 一致，只是返回了新的一个轴，而不是改变自身。

        Parameters
        ==========
        coplanar_axis: CoordinateAxis
            共面轴，默认为垂直于 Y 轴的另一个轴

        Returns
        =======
        CoordinateAxis
            垂直于当前轴的第三轴
        """

        if coplanar_axis is None:
            coplanar_axis = (
                CoordinateAxis.X if self == CoordinateAxis.Z else CoordinateAxis.Z
            )

        if self == coplanar_axis:  # 轴相同，无法形成平面
            raise AxisParallelError(
                "无法找到垂直于 {}-{} 平面的轴：所给二轴平行。".format(
                    coplanar_axis, self
                )
            )

        if coplanar_axis == CoordinateAxis.Y:
            if self == CoordinateAxis.X:
                return CoordinateAxis.Z
            else:  # self == CoordinateAxis.Z
                return CoordinateAxis.X
        elif coplanar_axis == CoordinateAxis.X:
            if self == CoordinateAxis.Y:
                return CoordinateAxis.Z
            else:  # self == CoordinateAxis.Z
                return CoordinateAxis.Y
        else:  # normal_axis == CoordinateAxis.Z
            if self == CoordinateAxis.X:
                return CoordinateAxis.Y
            else:  # self == CoordinateAxis.Y
                return CoordinateAxis.X


class AxisOrientation(Enum):
    """
    坐标轴方向枚举类
    """

    POSITIVE = 1
    NEGATIVE = -1

    def __init__(self, value: Union[bool, int]) -> None:
        if isinstance(value, bool):  # 兼容 bool
            self._value_ = 1 if value else -1
        else:
            self._value_ = 1 if value >= 0 else -1

        super().__init__()

    def positive(self) -> bool:
        """
        判断当前方向是否为正方向

        Parameters
        ==========
        无

        Returns
        =======
        bool
            True 表示正方向，False 表示负方向
        """
        return self.value == 1

    def __bool__(self) -> bool:
        return self.positive()

    def __str__(self) -> str:
        return "+" if self.positive else "-"

    def __neg__(self) -> "AxisOrientation":
        return AxisOrientation.NEGATIVE if self.positive() else AxisOrientation.POSITIVE

    def __int__(self) -> int:
        return self.value


@dataclass
class DirectionCoordinate:
    """
    方向坐标类
    """

    x: int
    y: int
    z: int

    def __init__(self, pos_x: int, pos_y: int, pos_z: int) -> None:
        self.x = pos_x
        self.y = pos_y
        self.z = pos_z

    def move(
        self, direction: "DirectionPointing", distance: int
    ) -> "DirectionCoordinate":
        """
        将当前坐标移动，并返回移动之后的坐标

        Parameters
        ==========
        direction: DirectionPointing
            移动方向
        distance: int
            移动距离

        Returns
        =======
        DirectionCoordinate
            移动后的坐标
        """
        if direction.coordinate_axis == CoordinateAxis.X:
            self.x += distance * direction.coordinate_orientation.value
        elif direction.coordinate_axis == CoordinateAxis.Y:
            self.y += distance * direction.coordinate_orientation.value
        else:
            self.z += distance * direction.coordinate_orientation.value
        return self

    def move_noupdate(
        self, direction: "DirectionPointing", distance: int
    ) -> "DirectionCoordinate":
        """
        仅返回移动后的坐标，不更改现有实例

        Parameters
        ==========
        direction: DirectionPointing
            移动方向
        distance: int
            移动距离

        Returns
        =======
        DirectionCoordinate
            移动后的坐标
        """

        return self.copy().move(direction, distance)

    def euclidean_distance(self, other: "DirectionCoordinate") -> float:
        """
        计算两个坐标之间的欧氏距离

        Parameters
        ==========
        other: DirectionCoordinate
            要计算的坐标

        Returns
        =======
        float
            两个坐标之间的欧氏距离
        """
        return sqrt(
            (self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2
        )

    def to_minecraft_relative(self) -> str:
        return "~{} ~{} ~{}".format(self.x, self.y, self.z)

    def __add__(self, other: "DirectionCoordinate") -> "DirectionCoordinate":
        """
        将两个坐标相加

        Parameters
        ==========
        other: DirectionCoordinate
            要相加的坐标

        Returns
        =======
        DirectionCoordinate
            相加后的坐标
        """
        return DirectionCoordinate(self.x + other.x, self.y + other.y, self.z + other.z)

    def __sub__(self, other: "DirectionCoordinate") -> "DirectionCoordinate":
        """
        将两个坐标相减

        Parameters
        ==========
        other: DirectionCoordinate
            要相减的坐标

        Returns
        =======
        DirectionCoordinate
            相减后的坐标
        """
        return DirectionCoordinate(self.x - other.x, self.y - other.y, self.z - other.z)

    def copy(self) -> "DirectionCoordinate":
        """
        返回当前坐标的副本

        Parameters
        ==========
        无

        Returns
        =======
        DirectionCoordinate
            副本
        """
        return DirectionCoordinate(self.x, self.y, self.z)


class DirectionPointing:
    """
    方向指向类
    """

    coordinate_axis: CoordinateAxis
    """
    轴
    """

    coordinate_orientation: AxisOrientation
    """
    指向
    """

    def __init__(
        self,
        axis: Union[CoordinateAxis, str],
        orientation: Union[AxisOrientation, bool, int],
    ):
        if isinstance(axis, str):
            self.coordinate_axis = CoordinateAxis(axis.upper())
        else:
            self.coordinate_axis = axis

        if isinstance(orientation, AxisOrientation):
            self.coordinate_orientation = orientation
        else:
            self.coordinate_orientation = AxisOrientation(orientation)

    @classmethod
    def from_string(cls, direction_str: str) -> "DirectionPointing":
        """
        从字符串中创建坐标轴方向对象

        Parameters
        ==========
        direction_str: str
            字符串，格式为 "X+" 或 "-X" 或 "+X" 或 "-X", 大小写不敏感

        Returns
        =======
        CoordinateDirection 类
        """
        direction_str = direction_str.upper()
        if direction_str[0] in ("X", "Y", "Z"):
            if direction_str[1] in ("+", "-"):
                return cls(CoordinateAxis(direction_str[0]), direction_str[1] == "+")
        elif direction_str[0] in ("+", "-"):
            if direction_str[1] in ("X", "Y", "Z"):
                return cls(CoordinateAxis(direction_str[1]), direction_str[0] == "+")

        raise ValueError(
            "无法从字符串 {} 中创建坐标轴方向对象：字符串格式错误".format(direction_str)
        )

    def oppsite(self) -> "DirectionPointing":
        """返回该方向的相反方向"""
        return DirectionPointing(self.coordinate_axis, -self.coordinate_orientation)

    def __str__(self) -> str:
        return "{}{}".format(
            self.coordinate_axis.value,
            self.coordinate_orientation,
        )
